using System;
using System.Collections.Generic;
using System.Net;
using System.Net.Sockets;

namespace Cdp
{
    public class AsyncUdpClient
    {
        private SocketAsyncEventArgsPool ReceivePool, SendPool;
        private BufferManager ReceiveBuffer, SendBuffer;
        private Socket ListenSocket;
        private int Connections = 10;
        private EndPoint Remote = null;

        public int BufferSize { get; private set; }

        public event AsyncUdpEventHandler OnReceived;

        public AsyncUdpClient(int bufferSize)
        {
            BufferSize = bufferSize;
            ReceiveBuffer = new BufferManager(BufferSize * Connections, BufferSize);
            SendBuffer = new BufferManager(BufferSize * Connections, BufferSize);
            ReceivePool = new SocketAsyncEventArgsPool(Connections);
            SendPool = new SocketAsyncEventArgsPool(Connections);

            ReceiveBuffer.InitBuffer();
            SendBuffer.InitBuffer();

            SocketAsyncEventArgs args;
            for (int i = 0; i < Connections; i++)
            {
                args = new SocketAsyncEventArgs();
                args.Completed += new EventHandler<SocketAsyncEventArgs>(Event_Completed);
                args.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
                args.UserToken = new AsyncUserToken();
                ReceiveBuffer.SetBuffer(args);
                ReceivePool.Push(args);
            }

            for (int i = 0; i < Connections; i++)
            {
                args = new SocketAsyncEventArgs();
                args.Completed += new EventHandler<SocketAsyncEventArgs>(Event_Completed);
                args.UserToken = null;
                SendBuffer.SetBuffer(args);
                SendPool.Push(args);
            }            
        }

        public void Listen(IPEndPoint localEP)
        {
            ListenSocket = new Socket(localEP.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
            ListenSocket.Bind(localEP);
            Logger.Information("ucp listen at {0}, buffer size {1}", localEP.ToString(), BufferSize);

            StartReceive();
        }

        public void Connect(IPEndPoint remote)
        { 
            ListenSocket = new Socket(remote.AddressFamily, SocketType.Dgram, ProtocolType.Udp);
            ListenSocket.Connect(remote);            
            Logger.Information("ucp connect to {0}, buffer size {1}", remote.ToString(), BufferSize);

            Remote = remote;
            StartReceive();
        }

        private void StartReceive()
        {
            if (!ReceivePool.IsEmpty())
            {
                SocketAsyncEventArgs e = ReceivePool.Pop();
                if (!ListenSocket.ReceiveFromAsync(e))
                {
                    ProcessReceive(e);
                }
            }
            else
                Logger.Error("receive pool is empty");
        }

        private void Event_Completed(object sender, SocketAsyncEventArgs e)
        {
            switch (e.LastOperation)
            {
                case SocketAsyncOperation.ReceiveFrom:
                    ProcessReceive(e);
                    break;
                case SocketAsyncOperation.SendTo:
                    ProcessSend(e);
                    break;
                default:
                    Logger.Error("invalid operation: {0}", e.LastOperation);
                    if (e.UserToken == null)
                        SendPool.Push(e);
                    else
                    {
                        e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
                        ReceivePool.Push(e);
                    }
                    break;
            }
        }

        private void ProcessReceive(SocketAsyncEventArgs e)
        {
            if (e.BytesTransferred > 0 && e.SocketError == SocketError.Success)
            {
                CdpDatagram data = new CdpDatagram();
                data.Buffer = e.Buffer;
                data.Offset = e.Offset;
                data.BytesTransferred = e.BytesTransferred;
                data.RemoteEndPoint = e.RemoteEndPoint;
                OnReceived?.Invoke(this, data);
            }
            else
            {
                Logger.Error("receive error, {0}", e.SocketError);
            }

            e.RemoteEndPoint = new IPEndPoint(IPAddress.Any, 0);
            if (!ListenSocket.ReceiveFromAsync(e))
            {
                ProcessReceive(e);
            }
            else if (!ReceivePool.IsEmpty())
            {
                SocketAsyncEventArgs arg = ReceivePool.Pop();
                if (!ListenSocket.ReceiveFromAsync(arg))
                {
                    ProcessReceive(arg);
                }
            }
        }

        public void SendAsync(byte[] data, int offset, int count)
        {
            SendAsync(Remote, data, offset, count);
        }

        public void SendAsync(EndPoint remote, byte[] data, int offset, int count)
        {
            if (data != null && remote != null)
            {
                SocketAsyncEventArgs args = null;
                if (!SendPool.IsEmpty())
                {
                    args = SendPool.Pop();
                }
                else
                {
                    Logger.Information("send pool is empty");
                    args = new SocketAsyncEventArgs();
                    args.Completed += new EventHandler<SocketAsyncEventArgs>(Event_Completed);
                    args.UserToken = null;
                    args.SetBuffer(new byte[BufferSize], 0, BufferSize);
                }
                args.RemoteEndPoint = remote;
                Array.ConstrainedCopy(data, offset, args.Buffer, 0, count);
                args.SetBuffer(0, count);
                if (!ListenSocket.SendToAsync(args))
                {
                    ProcessSend(args);
                }
            }
            else
                Logger.Error("send data or remote endpoint is null");
        }

        public void SendAsync(CdpDatagram data)
        {
            SendAsync(data.RemoteEndPoint, data.Buffer, data.Offset, data.BytesTransferred);
        }

        private void ProcessSend(SocketAsyncEventArgs e)
        {
            if (e.SocketError != SocketError.Success)
            {
                Logger.Error("send error, {0}", e.SocketError);
            }

            e.RemoteEndPoint = null;
            SendPool.Push(e);
        }

        public void Stop()
        {
            try
            {
                ListenSocket.Shutdown(SocketShutdown.Both);
                ListenSocket.Dispose();
            }
            catch (Exception ex)
            {
                Logger.Error("stop catch exception, {0}", ex.Message);
            }

            while (!SendPool.IsEmpty())
                SendBuffer.FreeBuffer(SendPool.Pop());

            while (!ReceivePool.IsEmpty())
                ReceiveBuffer.FreeBuffer(ReceivePool.Pop());
        }
    }
}